====================================================================================================
HƯỚNG DẪN TẠO TIỆN ÍCH NGUỒN TRUYỆN CHO ỨNG DỤNG NGHE TRUYỆN
NATIVE SOURCE API 2
====================================================================================================

MỤC ĐÍCH
--------

Tài liệu này dùng để tạo, sửa và kiểm thử một tiện ích nguồn truyện dạng file .lua.

Khi mở cuộc trò chuyện mới, người dùng chỉ cần gửi tài liệu này và URL website muốn làm tiện ích.
Trợ lý phải dùng tài liệu như đặc tả kỹ thuật, tự nghiên cứu website trước, viết file .lua hoàn chỉnh,
và chỉ yêu cầu người dùng hỗ trợ ở những bước bắt buộc phải thử trên thiết bị thật.

Tài liệu chỉ nói về việc tạo tiện ích. Không chứa lịch sử phát triển ứng dụng hoặc lỗi nội bộ không liên quan.

====================================================================================================
1. THÔNG TIN CẦN CÓ KHI TẠO NGUỒN
====================================================================================================

Tối thiểu cần:

- URL website.
- Nếu có thể, một URL truyện và một URL chương để kiểm thử.

Trợ lý phải tự xác định:

- cách tìm kiếm;
- cách lấy danh sách truyện;
- trang chi tiết;
- cách lấy toàn bộ chương;
- cách lấy nội dung chương;
- website dùng HTML, JSON API hay JavaScript;
- có cần cookie, token, header hoặc Browser hay không.

Chỉ cần người dùng hỗ trợ khi:

- website cần đăng nhập hoặc xác minh tương tác;
- website chỉ truy cập được từ mạng/thiết bị của người dùng;
- lỗi chỉ xuất hiện trên Android/WebView thật;
- cần báo cáo Native Source Debug để chẩn đoán.

====================================================================================================
2. CÁCH NGHIÊN CỨU WEBSITE
====================================================================================================

Luôn đi theo thứ tự sau:

1. Kiểm tra HTML trực tiếp.
2. Nếu HTML không đủ, tìm API JSON/AJAX/GraphQL.
3. Xác định pagination, cookie, token, headers.
4. Dùng pipeline nhiều request nếu cần.
5. Chỉ dùng Browser nếu dữ liệu chỉ xuất hiện sau JavaScript hoặc cần cookie/request do trang tạo.

Ưu tiên kiến trúc:

HTTP trực tiếp
→ HTTP nhiều bước
→ JSON API
→ transform/condition
→ Browser fallback khi thật sự cần.

Nếu Browser được dùng để lấy cookie hoặc URL API, nên quay lại HTTP càng sớm càng tốt.

====================================================================================================
3. CẤU TRÚC FILE TIỆN ÍCH
====================================================================================================

Một tiện ích là một file Lua trả về table:

return {
  api_version = 2,

  metadata = {
    id = "example-native",
    name = "Example",
    version = 1,
    author = "Bạn",
    website = "https://example.com",
    description = "Nguồn truyện Example",
    locale = "vi",
    type = "novel",
    updated = "2026-07-20"
  },

  source = {
    base_url = "https://example.com",
    allowed_hosts = {"example.com", "api.example.com"},

    permissions = {
      browser = false,
      network_capture = false,
      storage = false,
      hosts = {"example.com", "api.example.com"}
    },

    config = {},
    pipelines = {},
    hooks = {},

    actions = {
      search = {},
      detail = {},
      chapters = {},
      content = {}
    }
  }
}

====================================================================================================
4. METADATA, HOST VÀ PERMISSIONS
====================================================================================================

Metadata khuyến nghị:

id
    ID ổn định của nguồn. Nên dùng chữ thường, số, "-" hoặc "_".

name
    Tên hiển thị.

version
    Tăng khi cập nhật tiện ích.

author
    Tác giả tiện ích.

website
    Website nguồn.

description
    Mô tả ngắn.

locale
    Ví dụ "vi".

type
    Nguồn truyện chữ dùng "novel".

updated
    Ngày cập nhật YYYY-MM-DD.

Các field tùy chọn nếu cần phân phối/cập nhật:

min_app_version, update_url, update_sha256, checksum, repository_url, repository_name, regexp, nsfw.

Host:

source = {
  base_url = "https://example.com",
  allowed_hosts = {
    "example.com",
    "api.example.com",
    "cdn.example.com"
  }
}

Khai báo mọi domain mà nguồn có thể thực sự truy cập.

Permissions:

browser = true
    Chỉ khi có Browser step.

network_capture = true
    Chỉ khi cần wait/capture request mạng của Browser.

storage = true
    Chỉ khi cần lưu token/trạng thái nhỏ giữa các action.

Không xin quyền không sử dụng.

====================================================================================================
5. CONFIG
====================================================================================================

Ví dụ:

config = {
  DOMAIN = {
    title = "Máy chủ",
    ["default"] = "https://example.com",
    mode = "select",
    format = "single",
    values = {
      "https://example.com",
      "https://mirror.example.com"
    }
  },

  timeout = {
    title = "Thời gian chờ",
    ["default"] = "30000",
    mode = "input",
    format = "number"
  }
}

Dùng:

{config.DOMAIN}
{config.timeout}
{config.timeout??30000}

Mode/format thường dùng:

input + text
input + number
select + single
select + multiple
toggle

Lưu ý cú pháp Lua trong môi trường này: với khóa "default" và "when", dùng dạng ngoặc vuông:

["default"] = "30000"
["when"] = {...}

Không đặt config key trùng tên API runtime phổ biến như fetch, Engine, Response, localStorage, JSON,
Math, Date, console, document, URL, parseInt hoặc parseFloat.

====================================================================================================
6. CÁC ACTION
====================================================================================================

Các action:

categories
stories
latest
search
detail
chapters
content
comments

Nguồn thực tế cần ít nhất:

- một action để tìm/duyệt: search, stories hoặc latest;
- chapters;
- content.

Rất nên có detail.

Ý nghĩa:

categories
    Danh mục/thể loại.

stories
    Danh sách truyện theo một input, thường là URL thể loại.

latest
    Truyện mới cập nhật.

search
    Tìm theo query.

detail
    Thông tin truyện.

chapters
    Trả toàn bộ danh sách chương. Nếu website chia nhiều trang, action này phải tự gom hết.

content
    Trả nội dung chương.

comments
    Bình luận nếu website hỗ trợ.

====================================================================================================
7. CẤU TRÚC ACTION VÀ CONTEXT
====================================================================================================

Action đơn giản:

detail = {
  request = {
    id = "detail",
    url = "{input}",
    response = "html"
  },
  result = {
    type = "detail",
    from = "$detail",
    fields = {
      name = {selector = "h1"}
    }
  }
}

Action phức tạp:

chapters = {
  steps = {
    {request = {...}},
    {set = {...}},
    {assert = {...}},
    {foreach = {...}},
    {transform = {...}}
  },
  result = {...}
}

Context/template thường dùng:

{input}
{query}
{page}
{current_url}
{base_url}
{vars.story_id}
{config.timeout}
{config.timeout??30000}

Filter:

|urlencode
|trim
|lower
|upper
|json

Tham chiếu trong pipeline:

$last
$request_id
$request_id.data.items
$vars.name
$args.items
$responses.request_id.status
$responses.request_id.headers
$input
$query
$page

page là token phân trang do nguồn tự định nghĩa. Có thể là số trang, cursor hoặc URL trang tiếp theo.

====================================================================================================
8. HTTP REQUEST
====================================================================================================

GET HTML:

request = {
  id = "page",
  url = "{input}",
  method = "GET",
  response = "html",
  timeout = 30000,
  retries = 1
}

GET JSON:

request = {
  id = "search",
  url = "https://api.example.com/search",
  query = {
    q = "{query}",
    page = "{page??1}"
  },
  response = "json"
}

POST JSON:

request = {
  id = "api",
  url = "https://api.example.com/search",
  method = "POST",
  headers = {
    ["Content-Type"] = "application/json",
    ["Referer"] = "https://example.com"
  },
  body = {
    keyword = "{query}",
    token = "{vars.token}"
  },
  response = "json"
}

Field thường dùng:

id, url, method, query/queries, headers, body, response, charset, timeout, retries,
allow_http_error, transport, user_agent.

Response type:

html, json, text, base64, bytes, response, raw, auto.

Nguyên tắc:

- đặt id rõ ràng;
- dùng đúng response type;
- ưu tiên HTTP;
- không retry quá nhiều;
- nếu API cần token, lấy token ở step trước rồi request tiếp.

====================================================================================================
9. DESCRIPTOR: TRÍCH DỮ LIỆU
====================================================================================================

HTML:

name = {selector = ".title"}

Selector fallback:

name = {
  selector = {"h1.title", ".book-title", "h1"}
}

Attribute:

url = {
  selector = "a",
  attr = "href",
  absolute = true
}

HTML content:

content = {
  selector = ".chapter-content",
  mode = "html",
  remove = {"script", "iframe", ".ads"}
}

Node hiện tại khi map:

name = {self = true}
url = {self = true, attr = "href", absolute = true}

JSON path:

title = {path = "data.book.title"}
items = {path = "data.items"}

Parse HTML nằm trong JSON:

chapter_doc = {
  from = "$chapter_page",
  path = "html",
  parse = "html"
}

Danh sách lồng:

genres = {
  items = {selector = ".genres a"},
  fields = {
    name = {self = true},
    url = {self = true, attr = "href", absolute = true}
  }
}

Fallback:

target = {
  first = {
    {from = "$vars.url"},
    {from = "$input"},
    {value = "https://example.com/latest"}
  }
}

Các khả năng descriptor thường dùng:

selector, path, attr, self, mode, remove, absolute, parse, first, value, template, from,
["default"], replace, regex, group, flags, prefix, suffix, join, matches, contains, map,
map_default, as="number", as="boolean".

====================================================================================================
10. RESULT
====================================================================================================

Type:

items, list, categories, detail, content, value, raw.

DANH SÁCH TRUYỆN:

result = {
  type = "items",
  from = "$search",
  items = {path = "data.items"},
  fields = {
    name = "title",
    url = {path = "url", absolute = true},
    cover = {path = "cover", absolute = true},
    description = "description"
  },
  next = {path = "data.nextCursor"}
}

CATEGORIES:

result = {
  type = "categories",
  items = {
    {name = "Hot", url = "https://example.com/hot"}
  },
  fields = {
    name = "name",
    url = {path = "url", absolute = true}
  }
}

DETAIL:

result = {
  type = "detail",
  from = "$detail",
  fields = {
    name = {selector = "h1"},
    author = {selector = ".author"},
    cover = {selector = ".cover img", attr = "src", absolute = true},
    description = {selector = ".description", mode = "html"},
    ongoing = {selector = ".status", matches = "Đang ra"}
  }
}

CHAPTERS:

result = {
  type = "items",
  from = "$vars.all_chapters",
  fields = {
    name = "name",
    url = {path = "url", absolute = true}
  }
}

Mỗi chapter tối thiểu cần name + url.

CONTENT:

result = {
  type = "content",
  from = "$chapter",
  fields = {
    title = {selector = ".chapter-title"},
    content = {
      selector = ".chapter-content",
      mode = "html",
      remove = {"script", "iframe", ".ads", ".navigation"}
    },
    tts_media = {path = "audioUrl", absolute = true}
  }
}

Có thể bỏ tts_media nếu website không cung cấp media URL.

Nội dung chương phải được làm sạch để đọc và TTS tốt.

====================================================================================================
11. CÁC STEP QUAN TRỌNG
====================================================================================================

SET:

{
  set = {
    story_id = {
      from = "$book",
      selector = "[data-story-id]",
      attr = "data-story-id"
    },
    token = {
      from = "$api",
      path = "data.token"
    }
  }
}

Dùng sau đó:

$vars.story_id
{vars.story_id}

ASSERT:

{
  assert = {
    condition = {
      left = "$vars.story_id",
      op = "not_empty"
    },
    message = "Không lấy được story_id"
  }
}

APPEND:

{
  append = {
    into = "items",
    value = {from = "$page", path = "data.items"},
    flatten = true
  }
}

FOREACH NHIỀU TRANG:

{
  foreach = {
    range = {
      from = 1,
      to = {from = "$vars.page_count"},
      max = 200
    },
    as = "page_no",
    into = "all_chapters",
    flatten = true,

    steps = {
      {
        request = {
          id = "chapter_page",
          url = "https://example.com/api/chapters?page={vars.page_no}",
          response = "json"
        }
      },
      {
        set = {
          chapter_doc = {
            from = "$chapter_page",
            path = "html",
            parse = "html"
          }
        }
      }
    },

    collect = {
      from = "$vars.chapter_doc",
      items = {selector = ".chapter-list a"},
      fields = {
        name = {self = true},
        url = {self = true, attr = "href", absolute = true}
      }
    }
  }
}

Nguyên tắc chapters:

- tự gom hết các trang;
- đặt max hợp lý;
- loại chapter trùng;
- trả danh sách hoàn chỉnh.

====================================================================================================
12. CONDITION VÀ TRANSFORM
====================================================================================================

WHEN:

{
  ["when"] = {from = "$query"},
  set = {
    target = {
      template = "https://example.com/search?q={query|urlencode}"
    }
  }
}

IF:

{
  if_ = {
    condition = {
      left = "$vars.token",
      op = "not_empty"
    },
    then_steps = {...},
    else_steps = {...}
  }
}

Operator phổ biến:

truthy, falsy, empty, not_empty, exists,
equals, not_equals,
contains, not_contains, starts_with, ends_with,
matches, regex, in,
greater, greater_equal, less, less_equal.

Có thể lồng bằng all, any và ["not"].

Transform chuỗi:

trim, lower, upper, replace, regex_replace, split, join, substring, slice_text,
url_encode, url_decode, html_decode, base64_encode, base64_decode, json_encode, json_decode.

Transform collection:

flatten, reverse, first, last, slice, map, filter, unique, sort.

Ví dụ lọc + khử trùng:

{
  transform = {
    from = "$vars.items",
    operations = {
      {
        op = "filter",
        as = "item",
        condition = {
          left = "$vars.item.url",
          op = "not_empty"
        }
      },
      {op = "unique", by = "url"}
    },
    into = "clean_items"
  }
}

====================================================================================================
13. REUSABLE PIPELINE VÀ LUA HOOK
====================================================================================================

Reusable pipeline dùng cho logic lặp lại:

pipelines = {
  normalize_items = {
    steps = {
      {
        transform = {
          from = "$args.items",
          op = "unique",
          by = "url",
          into = "normalized_items"
        }
      }
    }
  }
}

Gọi:

{
  call_pipeline = {
    name = "normalize_items",
    args = {
      items = "$vars.raw_items"
    }
  }
}

Lua hook dùng khi transform không đủ:

hooks = {
  normalize_title = function(ctx, value)
    local title = ctx.string.trim(value or "")
    return ctx.regex.replace(title, "^Chương\\s+0*", "Chương ")
  end
}

Gọi:

{
  hook = {
    name = "normalize_title",
    input = "$vars.raw_title",
    into = "chapter_title"
  }
}

Hook chỉ dùng để xử lý dữ liệu. Không dùng hook để gửi HTTP, mở Browser, truy cập Java/Android hoặc file hệ thống.

====================================================================================================
14. STORAGE, CRYPTO VÀ ERROR HANDLING
====================================================================================================

STORAGE:

permissions.storage = true

Get:

{storage = {op = "get", key = "token", into = "token"}}

Set:

{storage = {op = "set", key = "token", value = "{vars.token}"}}

Remove/Clear:

{storage = {op = "remove", key = "token"}}
{storage = {op = "clear"}}

Storage chỉ nên giữ token/trạng thái nhỏ.

CRYPTO:

{
  crypto = {
    algorithm = "hmac-sha256",
    value = "{vars.payload}",
    key = "{config.API_SECRET}",
    encoding = "hex",
    into = "signature"
  }
}

Algorithm:

md5, sha1, sha256, sha512,
hmac-md5, hmac-sha1, hmac-sha256, hmac-sha512.

ERROR:

on_error = "stop"
on_error = "continue"

Fallback:

on_error = {
  mode = "fallback",
  fallback = {
    {
      request = {
        id = "mirror",
        url = "https://mirror.example.com/api/...",
        response = "json"
      }
    }
  }
}

Không dùng continue để che lỗi dữ liệu bắt buộc. Dùng assert khi ID/token không thể thiếu.

====================================================================================================
15. DYNAMIC WEB RUNTIME
====================================================================================================

Chỉ dùng khi HTTP không đủ.

permissions = {
  browser = true,
  network_capture = true,
  hosts = {"example.com", "api.example.com"}
}

Các operation chính:

launch/open
wait_selector
wait_url
wait_request
capture/requests
evaluate
html/snapshot
urls
cookies/cookie/handoff
block
close

LAUNCH:

{
  browser = {
    op = "launch",
    session = "page",
    url = "{input}",
    timeout = 30000
  }
}

WAIT SELECTOR:

{
  browser = {
    op = "wait_selector",
    session = "page",
    selectors = {".chapter-content", "#content"},
    timeout = 15000,
    into = "ready"
  }
}

WAIT REQUEST:

{
  browser = {
    op = "wait_request",
    session = "page",
    patterns = {"/api/chapter", "/graphql"},
    method = "GET",
    timeout = 15000,
    into = "request_info"
  }
}

CAPTURE + FETCH BẰNG HTTP:

{
  browser = {
    op = "capture",
    session = "page",
    patterns = {"/api/chapter"},
    latest = true,
    fetch = {
      id = "chapter_api",
      response = "json",
      timeout = 30000
    },
    into = "chapter_json"
  }
}

Đây là mô hình nên ưu tiên:

Browser chạy JS/tạo cookie
→ capture URL request
→ HTTP fetch dữ liệu
→ pipeline parse.

EVALUATE:

{
  browser = {
    op = "evaluate",
    session = "page",
    script = "JSON.stringify(window.__INITIAL_STATE__ || {})",
    response = "json",
    into = "initial_state"
  }
}

HTML SAU RENDER:

{
  browser = {
    op = "html",
    session = "page",
    into = "rendered_doc"
  }
}

Giới hạn quan trọng:

- capture GET/HEAD thuận lợi nhất;
- Android WebView không đảm bảo cung cấp POST body;
- nếu cần replay POST/PUT, phải biết và tự dựng body/headers;
- không coi Browser capture là Chrome DevTools đầy đủ.

====================================================================================================
16. CÁC MẪU THIẾT KẾ THƯỜNG GẶP
====================================================================================================

HTML đơn giản:

search/detail/chapters/content
→ GET HTML
→ CSS selector.

API JSON:

request JSON
→ JSON path
→ result.

Cần ID/token:

GET trang truyện
→ set ID/token
→ assert
→ request API.

Chapters nhiều trang:

lấy page_count/cursor
→ foreach
→ collect
→ flatten
→ unique
→ result toàn bộ chapters.

Website cần JavaScript:

Browser launch
→ wait selector/request
→ capture cookie/API
→ quay lại HTTP.

API cần chữ ký:

set payload
→ crypto
→ request với signature.

====================================================================================================
17. QUY TRÌNH TẠO NGUỒN TỪ ĐẦU
====================================================================================================

1. Khảo sát website.
2. Tạo metadata, base_url, hosts.
3. Làm search/latest/stories trước để xác nhận request + parser.
4. Làm detail.
5. Làm chapters và đảm bảo lấy đủ toàn bộ chương.
6. Làm content và làm sạch HTML.
7. Thêm categories/comments nếu có.
8. Chỉ thêm Browser nếu HTTP không đủ.
9. Cài file .lua và thử trên Android.
10. Nếu lỗi, dùng Native Source Debug để sửa chính xác.

====================================================================================================
18. CÀI VÀ KIỂM THỬ
====================================================================================================

Cài thủ công:

1. Lưu tiện ích thành file .lua.
2. Mở Quản lý tiện ích.
3. Chọn nhập/cài từ file.
4. Chọn file .lua.
5. Nếu validation thành công, mở nguồn và thử.

Checklist:

[ ] Cài được.
[ ] Tìm kiếm hoặc danh sách truyện có kết quả.
[ ] URL truyện đúng.
[ ] Detail đúng.
[ ] Chapters có toàn bộ chương.
[ ] Không trùng chapter.
[ ] Content tải được và sạch.
[ ] Phân trang danh sách truyện hoạt động nếu có.
[ ] Thử nhiều truyện khác nhau.
[ ] Thử một truyện có rất nhiều chương.
[ ] Nếu dùng Browser, thử lại sau khi đóng/mở ứng dụng.

====================================================================================================
19. KHI NÀO NGƯỜI DÙNG CẦN HỖ TRỢ
====================================================================================================

Người dùng thường chỉ cần:

1. Cài file .lua trên thiết bị.
2. Thử các chức năng.
3. Gửi báo cáo lỗi đầy đủ nếu có.

Có thể cần thêm:

- URL một truyện cụ thể;
- URL một chương;
- từ khóa tìm kiếm có kết quả;
- thực hiện đăng nhập/xác minh trên thiết bị;
- gửi Native Source Debug;
- gửi HTML hoặc JSON nếu website chỉ truy cập được từ phía người dùng.

Không cần yêu cầu người dùng tự tìm selector hoặc tự phân tích API nếu trợ lý có thể làm việc đó.

Khi website cần đăng nhập, không yêu cầu người dùng gửi mật khẩu. Họ có thể đăng nhập trực tiếp trên thiết bị.

====================================================================================================
20. CÁCH CHẨN ĐOÁN LỖI
====================================================================================================

INSTALL_VALIDATION_FAILED
    Kiểm tra cú pháp Lua, metadata, permissions, action, pipeline/hook và field schema.

HTTP ERROR
    Kiểm tra URL, host, status, headers, body, cookie, redirect, response type.

REQUEST OK NHƯNG KẾT QUẢ RỖNG
    Kiểm tra selector/JSON path, response thật, trang chống bot, dữ liệu render bằng JS.

CHAPTERS THIẾU
    Kiểm tra pagination, page_count/cursor, foreach max, collect và unique.

CONTENT RỖNG
    Kiểm tra selector, API chương, iframe/JS và remove selector.

BROWSER TIMEOUT
    Kiểm tra selector chờ, request pattern, host permission và bước xác minh.

Khi người dùng gửi DEBUG, phải đọc toàn bộ luồng và sửa nguyên nhân gốc, không chỉ vá thông báo cuối.

====================================================================================================
21. NGUYÊN TẮC THIẾT KẾ TỐT
====================================================================================================

- HTTP trước, Browser sau.
- Dùng API JSON nếu ổn định.
- Dùng selector fallback khi cần.
- Không hard-code dữ liệu có thể lấy động.
- Chỉ xin quyền thực sự dùng.
- Dùng assert cho dữ liệu bắt buộc.
- Chapters phải trả toàn bộ chương.
- Khử trùng chapter theo URL.
- Dùng reusable pipeline cho logic lặp lại.
- Dùng Lua hook chỉ khi transform không đủ.
- Làm sạch content cho TTS.
- Viết lỗi rõ ràng.
- Test nhiều truyện, không chỉ một mẫu.

====================================================================================================
22. TEMPLATE KHỞI ĐẦU
====================================================================================================

Template này là khung tham khảo. Phải thay URL, selector và API theo website thật.

return {
  api_version = 2,

  metadata = {
    id = "example-native",
    name = "Example",
    version = 1,
    author = "Bạn",
    website = "https://example.com",
    description = "Nguồn truyện Example",
    locale = "vi",
    type = "novel",
    updated = "2026-07-20"
  },

  source = {
    base_url = "https://example.com",
    allowed_hosts = {"example.com", "api.example.com"},

    permissions = {
      browser = false,
      network_capture = false,
      storage = false,
      hosts = {"example.com", "api.example.com"}
    },

    config = {
      timeout = {
        title = "Thời gian chờ",
        ["default"] = "30000",
        mode = "input",
        format = "number"
      }
    },

    pipelines = {
      normalize_items = {
        steps = {
          {
            transform = {
              from = "$args.items",
              operations = {
                {
                  op = "filter",
                  as = "item",
                  condition = {
                    left = "$vars.item.url",
                    op = "not_empty"
                  }
                },
                {op = "unique", by = "url"}
              },
              into = "normalized_items"
            }
          }
        }
      }
    },

    hooks = {
      normalize_title = function(ctx, value)
        local title = ctx.string.trim(value or "")
        return ctx.regex.replace(title, "^Chương\\s+0*", "Chương ")
      end
    },

    actions = {
      search = {
        request = {
          id = "search",
          url = "https://api.example.com/search",
          query = {
            q = "{query}",
            cursor = "{page}"
          },
          response = "json",
          timeout = "{config.timeout??30000}"
        },
        result = {
          type = "items",
          from = "$search",
          items = {path = "data.items"},
          fields = {
            name = "title",
            url = {path = "url", absolute = true},
            cover = {path = "cover", absolute = true},
            description = "description"
          },
          next = {path = "data.nextCursor"}
        }
      },

      detail = {
        request = {
          id = "detail",
          url = "{input}",
          response = "html"
        },
        result = {
          type = "detail",
          from = "$detail",
          fields = {
            name = {selector = {"h1.title", "h1"}},
            author = {selector = ".author"},
            cover = {selector = ".cover img", attr = "src", absolute = true},
            description = {selector = ".description", mode = "html"}
          }
        }
      },

      chapters = {
        steps = {
          {
            request = {
              id = "book",
              url = "{input}",
              response = "html"
            }
          },
          {
            set = {
              book_id = {
                from = "$book",
                selector = "[data-book-id]",
                attr = "data-book-id"
              }
            }
          },
          {
            assert = {
              condition = {
                left = "$vars.book_id",
                op = "not_empty"
              },
              message = "Không lấy được ID truyện"
            }
          },
          {
            request = {
              id = "toc",
              url = "https://api.example.com/books/{vars.book_id}/chapters",
              response = "json"
            }
          },
          {
            set = {
              raw_items = {
                from = "$toc",
                path = "data.chapters"
              }
            }
          },
          {
            call_pipeline = {
              name = "normalize_items",
              args = {
                items = "$vars.raw_items"
              }
            }
          }
        },
        result = {
          type = "items",
          from = "$vars.normalized_items",
          items = {self = true},
          fields = {
            name = "title",
            url = {path = "url", absolute = true}
          }
        }
      },

      content = {
        steps = {
          {
            request = {
              id = "chapter",
              url = "{input}",
              response = "html"
            }
          },
          {
            set = {
              raw_title = {
                from = "$chapter",
                selector = ".chapter-title"
              }
            }
          },
          {
            hook = {
              name = "normalize_title",
              input = "$vars.raw_title",
              into = "chapter_title"
            }
          }
        },
        result = {
          type = "content",
          from = "$chapter",
          fields = {
            title = "$vars.chapter_title",
            content = {
              selector = ".chapter-content",
              mode = "html",
              remove = {"script", "iframe", ".ads", ".navigation"}
            }
          }
        }
      }
    }
  }
}

====================================================================================================
23. PROMPT DÙNG Ở CUỘC TRÒ CHUYỆN MỚI
====================================================================================================

"Hãy đọc tài liệu Native Source API 2 tôi gửi kèm và dùng nó làm đặc tả để tạo tiện ích nguồn truyện
cho website: [URL]. Hãy tự nghiên cứu website trước, ưu tiên HTTP/API và chỉ dùng Browser khi cần.
Hãy tạo một file .lua hoàn chỉnh có chức năng duyệt hoặc tìm kiếm truyện, detail, toàn bộ danh sách
chương và nội dung chương. Nếu có bước chỉ kiểm thử được trên Android thật, hãy nói rõ tôi cần làm gì.
Khi tôi gửi Native Source Debug, hãy dựa vào báo cáo để sửa nguyên nhân gốc."

====================================================================================================
KẾT THÚC
====================================================================================================
